<?php

namespace Aspic\Model;

use Aspic as A;
use Aspic\Security\Security as S;

/**
 * Provide a DataModel that could be used to generate forms, database tables instances, etc.
 * Currently, only one column could be register as primary key, it is not possible to make a primary key with more than one column.
 * The DataModel could be optionnaly associate with a CrudModelInterface
 */
class DataModel {

	const ROW_CLASS = 'Aspic\\Model\\DynamicRow';

	protected $_defaultColParams = array(
		// Column name
		'name'				 => '',
		// Column label value or label translation id
		'label'				 => '',
		// PHP scalar type of the column
		'type'				 => null,
		// Length of the column type
		'typeLength'		 => null,
		// Special Type class (optionnal, see STRow)
		'specialTypeClass'	 => null,
		// Possible values for the column value (optionnal)
		'possibleValues'	 => null,
		// Define if column value can be null
		'canBeNull'			 => true,
		// Default value of the column
		'defaultValue'		 => null,
		// AutoIncrement column
		'autoIncrement'		 => false,
	);

	protected $_columns;

	protected $_primaryKey;

	/**
	 *
	 * @var array Keys :
	 * colName
	 * dataModel
	 * propertyName
	 */
	protected $_foreignKeys;

	protected $_oppositeForeignKeys;

	protected $_crudModel;

	protected $_rowClass;

	public function __construct($rowClass = null) {
		$this->_columns = array();
		$this->_foreignKeys = array();
		$this->_oppositeForeignKeys = array();
		$this->_primaryKey = null;
		$this->_crudModel = null;

		$this->setRowClass($rowClass);
	}

	/**
	 * Used to set foreign keys and then make links between differents DataModel because they cannot all be instanciated at the same time
	 * @param array $dependencies Dependencies of the DataModel (service)
	 * @see \Aspic\Service\ServiceInterface
	 */
	public function setForeignKeys(array $dependencies) {

	}

	/**
	 *
	 * @param CrudModelInterface $CrudModelInterface A model that correspond to the persistant
	 */
	public function setCrudModel(CrudModelInterface $crudModel) {
		$this->_crudModel = $crudModel;
	}

	/**
	 *
	 * @return CrudModelInterface
	 */
	public function getCrudModel() {
		return $this->_crudModel;
	}

	public function hasCrudModel() {
		return ($this->getCrudModel() != null);
	}

	/**
	 * Set the column that is the primary key (a column that will be used to identify one record amoung the others)
	 * @param string $primaryKeyCol Name of the column that is the primary key
	 */
	public function setPrimaryKey($colName) {
		if (!$this->colExists($colName)) {
			throw new A\Exception('Column does not exists');
		}
		else {
			$this->_primaryKey = $colName;
		}
	}

	public function setRowClass($classPath) {
		if (is_null($classPath)) {
			$this->_rowClass = static::ROW_CLASS;
		}
		else {
//				Cannot be used until PHP 5.3.9
//				if(!is_subclass_of($classPath, static::ROW_CLASS) AND $classPath != static::ROW_CLASS, true) {
			$refGiven = new \ReflectionClass($classPath);
			$refRowClass = new \ReflectionClass(static::ROW_CLASS);

			if (!$refGiven->isSubclassOf($refRowClass)) {
				throw new A\Exception('Class ' . $classPath . ' is not an instance of ' . static::ROW_CLASS);
			}

			$this->_rowClass = $classPath;
		}
	}

	public function getRowClass() {
		return $this->_rowClass;
	}

	public function rowClassTakeDataModel() {
		$rowClass = $this->getRowClass();

		$ref = new \ReflectionClass($rowClass);
		if ($ref->getConstant('TAKE_DATA_MODEL')) {
//				var_dump($this);
		}
		return $ref->getConstant('TAKE_DATA_MODEL');
	}

	/**
	 *
	 * @param array $givenParams
	 * @return Row
	 */
	public function getRowInstance(array $givenParams = array()) {
		$rowClass = $this->_rowClass;
		$ref = new \ReflectionClass($rowClass);

		$params = array();

		if ($ref->getConstant('TAKE_DATA_MODEL')) {
			$params[] = $this;
		}

		$params = array_merge($params, $givenParams);

		return $ref->newInstanceArgs($params);
	}

	/**
	 * Add a column to the model
	 * @param string|array $type One of the PHP scalar type or an array(phpScalarType, typeLength) like array('string', 10)
	 * @param array|null $possibleValues Possible values for the column (array('value' => 'label', 'value2' => 'label2')). Set to null if there is not predifined values
	 */
	public function addEnumCol($name, $type = 'string', $label = '', $canBeNull = true, $possibleValues = null, $defaultValue = null) {
		$colData = array(
			'name'			 => $name,
			'label'			 => $label,
			'possibleValues' => $possibleValues,
			'canBeNull'		 => $canBeNull,
			'defaultValue'	 => $defaultValue,
		);

		$colData = array_merge($colData, $this->parseType($type));

		return $this->addColFromArray($colData);
	}

	public function addCol($name, $type = 'string', $label = '', $canBeNull = true, $defaultValue = null) {
		$colData = array(
			'name'			 => $name,
			'label'			 => $label,
			'canBeNull'		 => $canBeNull,
			'defaultValue'	 => $defaultValue,
		);

		$colData = array_merge($colData, $this->parseType($type));

		return $this->addColFromArray($colData);
	}

	/**
	 * Add auto increment primary key
	 */
	public function addAICol($name, $label = '') {
		$colData = array(
			'name'			 => $name,
			'label'			 => $label,
			'autoIncrement'	 => true,
		);

		return $this->addColFromArray($colData);
	}

	/**
	 * @param string $propertyName The name used to retrieve the foreign key object automatically from the current object
	 */
	public function addForeignKey($colName, $propertyName, DataModel $foreignModel, $foreignPropertyName) {
		$this->_foreignKeys[$colName] = array(
			'colName'		 => $colName, // This column of me,
			'dataModel'		 => $foreignModel, // is linked to this DataModel
			'propertyName'	 => $propertyName,
		);

		$foreignModel->addOppositeForeignKey($colName, $this, $foreignPropertyName);
	}

	public function removeForeignKey($colName) {
		$this->_foreignKeys[$colName]['dataModel']->removeOppositeForeignKey($colName, $this);

		unset($this->_foreignKeys[$colName]);

		return true;
	}

	/**
	 * Register a one-to-many relation (a foreign key from another DataModel to this DataModel)
	 * This method is automatically called to the foreign data model in addForeignKey
	 * @see addForeignKey
	 *
	 * The name used as property name of an row object using DM that have foreign key to this DM, to retrieve data coresponding to this dataModel (one to many relation)
	 * Usually, it is just the dataModel entity name + "s".
	 * "post" => "posts", "user" => "users"
	 * Ex: A forum contains posts; what property name should be used to get the post of the forum ?
	 */
	protected function addOppositeForeignKey($oppositeForeignModelKeyName, DataModel $foreignModel, $propertyName) {
		$this->_oppositeForeignKeys[] = array(
			'colName'		 => $oppositeForeignModelKeyName, // This column,
			'dataModel'		 => $foreignModel, // of this DataModel has a link to me
			'propertyName'	 => $propertyName,
		);
	}

	protected function removeOppositeForeignKey($foreignColName, DataModel $foreignModel) {
		foreach ($this->_oppositeForeignKeys as $k => $ofk) {
			if ($ofk['colName'] == $foreignColName AND $ofk['dataModel'] == $foreignModel) {
				unset($this->_oppositeForeignKeys[$k]);

				return true;
			}
		}

		throw new A\Exception('Cannot remove opposite foreign key');
	}

	public function addColFromArray(array $colData) {
		if (isset($colData['autoIncrement'])) {
			$colData['type'] = 'integer';
		}

		$out = array_merge($this->_defaultColParams, $colData);
//			var_dump($out);
		$this->_columns[$out['name']] = $out;
	}

	/**
	 * Parse type arg and return an array with type data
	 *
	 * @param array|string $type A php saclar type like "integer", "string" or an array array(type, typeLength) like array('string', 15);
	 */
	public function parseType($type) {
		$colData = array();

		if (is_array($type)) {
			$colData['type'] = $type[0];
			$colData['typeLength'] = $type[1];
		}
		else {
			$colData['type'] = $type;
		}

		$colData['type'] = strtolower($colData['type']);

		return $colData;
	}

	public function setColSpecialType($colName, $specialType) {
		$col = $this->getCol($colName, true);

		$col['specialType'] = $specialType;
	}

	/**
	 * @return array The primary key column
	 */
	public function getPrimaryKey() {
		$pkName = $this->getPrimaryKeyName();

		return $this->_columns[$pkName];
	}

	public function getPrimaryKeyName() {
		return $this->_primaryKey;
	}

	public function hasPrimaryKey() {
		return ($this->getPrimaryKeyName() != null);
	}

	public function isPrimaryKeyAI() {
		return $this->getPrimaryKey()['autoIncrement'];
	}

	public function getCol($colName, $checkExists = false) {
		if (!$checkExists OR $this->colExists($colName)) {
			return $this->_columns[$colName];
		}
		else {
			return null;
		}
	}

	public function getCols() {
		return $this->_columns;
	}

	public function getColsNames() {
		return array_keys($this->_columns);
	}

	public function colExists($name) {
		return isset($this->_columns[$name]);
	}

	public function getForeignKeys() {
		return $this->_foreignKeys;
	}

	public function getOppositeForeignKeys() {
		return $this->_oppositeForeignKeys;
	}

	public function getForeignKey($colName, $checkExists = false) {
		if (!$checkExists OR $this->foreignKeyExists($colName)) {
			return $this->_foreignKeys[$colName];
		}
		else {
			return null;
		}
	}

	/**
	 *
	 * @param string $propertyName
	 * @return array|null The foreign key data or null if not found
	 */
	public function getForeignKeyByPropertyName($propertyName) {
		$index = A\UArray::inLinesSearch($this->_foreignKeys, $propertyName, 'propertyName');

		if ($index !== false) {
			return $this->getForeignKey($index);
		}
		else {
			return null;
		}
	}

	/**
	 *
	 * @param string $propertyName
	 * @return array|null The opposite foreign key data or null if not found
	 */
	public function getOppositeForeignKeyByPropertyName($propertyName) {
		$index = A\UArray::inLinesSearch($this->_oppositeForeignKeys, $propertyName, 'propertyName');

		if ($index !== false) {
			return $this->_oppositeForeignKeys[$index];
		}
		else {
			return null;
		}
	}

	public function foreignKeyExists($colName) {
		return isset($this->_foreignKeys[$colName]);
	}

	public function hasForeignKeys() {
		return (count($this->getForeignKeys()) > 0);
	}

	public function hasOppositeForeignKeys() {
		return (count($this->getOppositeForeignKeys()) > 0);
	}

	/**
	 * Return an instance of a sub Data Model Row class
	 * @param string $objectNotation
	 * @return Row
	 * @throws A\Exception
	 */
	public function getSubDataModelRowInst($objectNotation) {
		$dataModel = $this->getSubDataModel($objectNotation);

		$inst = $dataModel->getRowInstance();

		return $inst;
	}

	/**
	 * Return a data model from object notation of foreign keys
	 * @param string|array $objectNotation
	 * @return DataModel
	 * @throws A\Exception
	 */
	public function getSubDataModel($objectNotation) {
		$arrPath = A\UString::getObjectNotationArray($objectNotation);

		$dataModel = $this;

		foreach ($arrPath as $fkProperty) {
			$fk = $dataModel->getForeignKeyByPropertyName($fkProperty);

			if ($fk == null) {
				throw new A\Exception('Cannot find foreign key from property name "' . $fkProperty . '" (in ' . $objectNotation . ')');
			}

			$dataModel = $fk['dataModel'];
		}

		return $dataModel;
	}

	/**
	 * Construct a DataModel from a table of the database
	 * @param type $tableName
	 * @return \Aspic\Database\TableDataModel
	 * @todo Use of null, type and default to generate it
	 */
	public static function buildTableDataModel($tableName, A\Database\AbstractDatabase $database) {
		$dataModel = new TableDataModel($tableName);

		$cols = $this->fetchTableColumns($tableName);

		foreach ($cols as $col) {
			if ($col['extra'] == 'auto_increment') {
				$dataModel->addAICol($col['name']);
			}
			else {
				$dataModel->addCol($col['name']);
			}

			if ($col['key'] == 'PRI') {
				$pkName = $col['name'];
			}
		}

		if (isset($pkName)) {
			$dataModel->setPrimaryKey($pkName);
		}

		return $dataModel;
	}

}
